﻿using System;
using System.Collections.Generic;
using System.Collections;
using System.Text;
using TCSA.Graphics.GeomEngine;
using TCSA.Utils.Coordinates;
using System.IO;




namespace Geoconcepts.SpatialServices
{

    public class AreaSorter : IComparer
    {

        // Calls CaseInsensitiveComparer.Compare with the parameters reversed.
        int IComparer.Compare(Object x, Object y)
        {
            if (!((x is cElement) & (y is cElement)))
            {
                throw new ArgumentException("Los objetos deben ser de tipo cElement.");
            }

            cElement oElementX = (cElement)x;
            cElement oElementY = (cElement)y;

            return (int)Math.Ceiling(oElementX.Geometry.GetArea() - oElementY.Geometry.GetArea());

        }
    }


    #region SpatialConversions
    /// <summary>
    /// Proporciona servicios para la conversión de formatos de coordenadas
    /// </summary>
    public static class SpatialConversions
    {
        ///// <summary>
        ///// Convierte las coordenadas de un punto de geográficas a UTM
        ///// </summary>
        ///// <param name="drPoint">Punto cuyas coordenadas se van a convertir.</param>
        ///// <returns>Punto con las coordenadas convertidas.</returns>
        //public static tPoint3D ConvertToUTM(DataRow drPoint, int huso)
        //{
        //    try
        //    {
        //        double dLongitude = Convert.ToDouble(drPoint["longitude"], ciEnglish);
        //        double dLatitude = Convert.ToDouble(drPoint["latitude"], ciEnglish);
        //        cGeodesicalCoordinate oGeoCoordinate = new cGeodesicalCoordinate(eDatum.ETRS89, dLongitude, dLatitude);
        //        cUTMCoordinate oUTMCoordinate = new cUTMCoordinate();
        //        oUTMCoordinate = oGeoCoordinate.ToUTM(huso);
        //        tPoint3D oNewPoint = new tPoint3D();

        //        oNewPoint.x = oUTMCoordinate.X;
        //        oNewPoint.y = oUTMCoordinate.Y;
        //        return oNewPoint;

        //    }
        //    catch (Exception exc)
        //    {
        //        throw (exc);
        //    }
        //}



        /// <summary>
        /// Pasa un ángulo de grados a radianes
        /// </summary>
        /// <param name="angle"></param>
        /// <returns></returns>
        public static double GraToRad(double angle)
        {
            try
            {                
                return angle * Math.PI / 180;
            }
            catch(Exception exc)
            {
                throw (exc);
            }

        }

        /// <summary>
        /// Convierte un ángulo geográfico en trigonométrico (cambia los ejes y el sentido de giro)
        /// </summary>
        /// <param name="angle">Ángulo geográfico en grados</param>
        /// <param name="mode">0= grados; 1 = radianes</param>
        /// <returns></returns>
        public static double GeograhicToTrigonometric(double angle, int mode)
        {
            try
            {
                double dTrigAngle = angle;
                if (mode == 0)
                {
                    dTrigAngle = 450 - angle;
                    while (dTrigAngle > 360)
                    {
                        dTrigAngle -= 360;
                    }
                    return dTrigAngle;
                }
                else if (mode == 1)
                {
                    double d2PI = 2 * Math.PI;
                    dTrigAngle = 2.5 * Math.PI - angle;
                    while (dTrigAngle > d2PI)
                    {
                        dTrigAngle -= d2PI;
                    }
                    return dTrigAngle;

                }
                else
                {
                    throw(new Exception("Modo no válido"));
                }

            }
            catch(Exception exc)
            {
                throw (exc);
            }
        }

        /// <summary>
        /// Convierte las coordenadas de un punto geográficas a UTM.
        /// </summary>
        /// <param name="element">Punto cuyas coordenadas se van a convertir.</param>
        /// <param name="huso">Huso geográfico en el que se ubica el elemento.</param>
        /// <returns>Punto con las coordenadas convertidas.</returns>
        public static tPoint3D ConvertToUTM(tPoint3D point, int huso)
        {
            try
            {
                tPoint3D oPoint = point.Clone();
               Hashtable htFormats = tPoint3D.AvailableSystemSRSs(false);
               


                cGeodesicalCoordinate oGeoCoordinate = new cGeodesicalCoordinate(eDatum.ETRS89, point.x, point.y);
                cUTMCoordinate oUTMCoordinate = new cUTMCoordinate();
                oUTMCoordinate = oGeoCoordinate.ToUTM(huso);
                oPoint.x = oUTMCoordinate.X;
                oPoint.y = oUTMCoordinate.Y;
            
                return oPoint;
            }
            catch (Exception exc)
            {
                throw (exc);
            }
        }

        /// <summary>
        /// Convierte las coordenadas de geográficas a UTM.
        /// </summary>
        /// <param name="element">Elemento cuyas coordenadas se van a convertir.</param>
        /// <param name="huso">Huso geográfico en el que se ubica el elemento.</param>
        /// <returns>Elemento con las coordenadas convertidas.</returns>
        public static cElement ConvertToUTM(cElement element, int huso)
        {
            try
            {
                //Hashtable htFormats = tPoint3D.AvailableSystemSRSs(false);
                cElement oElement = element.Clone();
                for (int i = 0; i < oElement.Geometry.pPoints.Length; i++)
                {
                    cGeodesicalCoordinate oGeoCoordinate = new cGeodesicalCoordinate(eDatum.ETRS89, oElement.Geometry.pPoints[i].x, oElement.Geometry.pPoints[i].y);

                    cUTMCoordinate oUTMCoordinate = new cUTMCoordinate();
                    oUTMCoordinate = oGeoCoordinate.ToUTM(huso);
                    //tPoint3D oUTMPoint = oElement.Geometry.pPoints[i].TransformSRS(4326, 4230);
                    oElement.Geometry.pPoints[i].x = oUTMCoordinate.X;
                    oElement.Geometry.pPoints[i].y = oUTMCoordinate.Y;
                }
                oElement.GetCentroid(true);
                oElement.Geometry.CloseRings();
                oElement.Geometry.FixCounterClockwiseRings();

                cExtent oExtent = oElement.GetExtent(true);
                
                
                return oElement;
            }
            catch (Exception exc)
            {
                throw (exc);
            }
        }

        /// <summary>
        /// Convierte las coordenadas de un punto a geograficas
        /// </summary>
        /// <param name="element"></param>
        /// <param name="huso"></param>
        /// <returns></returns>
        public static tPoint3D ConvertToGeo(tPoint3D element, int huso)
        {
            tPoint3D oElement = element.Clone();

            cUTMCoordinate oUTMCCoordenadas = new cUTMCoordinate(eDatum.ETRS89, element.x, element.y, huso);
            cGeodesicalCoordinate oGeoCoordinate = new cGeodesicalCoordinate();
            oGeoCoordinate = oUTMCCoordenadas.ToGeodesical();

            oElement.x = oGeoCoordinate.Longitude.Degrees;
            oElement.y = oGeoCoordinate.Latitude.Degrees;

            return oElement;
        }
        /// <summary>
        /// Convierte las coordenadas de un elemento de UTM a geográficas. 
        /// </summary>
        /// <param name="element">Elemento cuyas coordenadas se van a convertir.</param>
        /// <returns>Elemento con las coordenadas convertidas.</returns>
        public static cElement ConvertToGeo(cElement element, int huso)
        {
            try
            {
                cElement oElement = element.Clone();
                for (int i = 0; i < oElement.Geometry.pPoints.Length; i++)
                {
                    cUTMCoordinate oUTMCCoordenadas = new cUTMCoordinate(eDatum.ETRS89, oElement.Geometry.pPoints[i].x, oElement.Geometry.pPoints[i].y, huso);
                    cGeodesicalCoordinate oGeoCoordinate = new cGeodesicalCoordinate();
                    oGeoCoordinate = oUTMCCoordenadas.ToGeodesical();

                    oElement.Geometry.pPoints[i].x = oGeoCoordinate.Longitude.Degrees;
                    oElement.Geometry.pPoints[i].y = oGeoCoordinate.Latitude.Degrees;
                }
                cExtent oExtent = oElement.GetExtent(true);
                return oElement;
            }
            catch (Exception exc)
            {
                throw (exc);
            }

        }

        public static int CalculateUTMZone(tPoint3D point)
        {
            int iHuso;
            double dLongAbsMin = point.x;
            if (point.x < 0)
                dLongAbsMin += 360;

            iHuso = (int)(dLongAbsMin / 6) + 31;
            if (iHuso > 60)
                iHuso -= 60;
            return iHuso;
        }

    }
    #endregion

    #region SpatialOperations

    /// <summary>
    /// Proporciona servicis de consulta espacial sobre recintos contenidos en un archivo GIS.
    /// </summary>
    public class SpatialOperations
    {

 
        private int miHuso;


        /// <summary>
        /// Huso geográfico en el que se ubica el contenido del archivo GIS.
        /// </summary>
        public int Huso
        {
            get { return miHuso; }
            //set { miHuso = value; }
        }
        private cGisFile moGisFile;

        /// <summary>
        /// Archivo GIS sobre el que se realizan las operaciones espaciales.
        /// </summary>
        public cGisFile GisFile
        {
            get { return moGisFile; }
        }

        private cElements moGeoConcepts;

        public cElements GeoConcepts
        {
            get { return moGeoConcepts; }           
        }
        private cElements moUTMGeoConcepts;

        public cElements UTMGeoConcepts
        {
            get { return moUTMGeoConcepts; }            
        }
        private SortedList mslGisFiles = new SortedList();
        private SortedList mslUTMGeoconcepts = new SortedList();

        /// <summary>
        /// Constructor de la clase.
        /// </summary>
        /// <param name="gisFile">Archivo GIS sobre el que se realizan las operaiones espaciales.</param>
        /// <param name="huso">Huso geográfico en el que se ubica contenido del archivo GIS</param>
        public SpatialOperations()
        {

            string sKmlPath = AppDomain.CurrentDomain.BaseDirectory + @"\bin\KmlFiles";

            DirectoryInfo diKmls = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory + @"\bin\KmlFiles");
            FileInfo[] fiKmls = diKmls.GetFiles();


            //Abro los GIS File. De momento los abro todos ya que en principio van a ser tres. Habrá que estudiar cómo
            //almacenamos esto en un GeoDataset.
            foreach (FileInfo fiKml in fiKmls)
            {
                cGisFile oGisFile = new cGisFile();
                oGisFile.Open(fiKml.FullName, true, true);
                mslGisFiles.Add(fiKml.Name.Remove(fiKml.Name.Length - 4), oGisFile);

                moGeoConcepts = oGisFile.ToElements();
                moUTMGeoConcepts = new cElements();

                foreach (cElement oElement in moGeoConcepts)
                {
                    
                    moUTMGeoConcepts.Add(SpatialConversions.ConvertToUTM(oElement, miHuso));                    
                    
                }
                mslUTMGeoconcepts.Add(fiKml.Name.Remove(fiKml.Name.Length - 4), moUTMGeoConcepts);
            }


           
            
            
            //moUTMGeoConcepts = new cElements();
            //foreach (cElement oElement in moGeoConcepts)
            //{
            //    moUTMGeoConcepts.Add(SpatialConversions.ConvertToUTM(oElement, miHuso));
            //}            
        }

  
        /// <summary>
        /// Calculates the time zone that contains a point
        /// </summary>
        /// <param name="longitude"></param>
        /// <param name="latitude"></param>
        /// <returns></returns>
        public int GetHuso(double longitude, double latitude)
        {
            try
            {
                //Por si un fichero está en más de un huso refiero todo al menor.
                int iHuso;
               
                double dLongAbsMin = longitude;
                if (longitude < 0)
                    dLongAbsMin += 360;
                

                iHuso = (int)(dLongAbsMin / 6) + 31;
                if (iHuso > 60)
                    iHuso -= 60;

                return iHuso;
            }
            catch (Exception exc)
            {
                throw (exc);
            }
        }

        /// <summary>
        /// Establece la ciudad en la que se encuentra el punto
        /// </summary>
        /// <param name="longitude"></param>
        /// <param name="latitude"></param>
        /// <returns></returns>
        public string GetCity(double longitude, double latitude)
        {
            try
            {
                foreach (string key in mslGisFiles.Keys)
                {
                    cGisFile oGisFile = (cGisFile)mslGisFiles[key];
                    cElements oUTMGeoConcepts = (cElements)mslUTMGeoconcepts[key];
                    cExtent oExtent = oGisFile.pBounds;
                    if (oExtent.Contains2D(longitude, latitude))
                    {
                        moGisFile = oGisFile;
                        moUTMGeoConcepts = oUTMGeoConcepts;          
                        //Obtengo el huso en el que está la coordenada inferior
                        miHuso = GetHuso(oExtent.xLow, oExtent.yLow);
                        return key;
                    }
                }
                moGisFile = null;
                return "";
            }
            catch(Exception exc)
            {
                throw (exc);
            }
        }

        /// <summary>
        /// Obtiene los elementos gráficos (en UTM) que contienen un punto dado ordenados de menor a mayor.
        /// </summary>
        /// <param name="longitude">Longitud</param>
        /// <param name="latitude">Latitud</param>
        /// <returns></returns>
        public cElements GetContainerElements(double longitude, double latitude)
        {
            try
            {                
                tPoint3D p3dCurrent = new tPoint3D(longitude, latitude);
                tPoint3D p3dCurrentUTM = SpatialConversions.ConvertToUTM(p3dCurrent, miHuso);
                cElements oElements = moUTMGeoConcepts.CheckSpatial(p3dCurrentUTM, eSearchingModes.Within);
                oElements.Sort(new AreaSorter());

                return oElements;
            }
            catch (Exception exc)
            {
                throw (exc);
            }

        }

        /// <summary>
        /// Devuelve los elementos que cortan un segmento dado en forma de origen, dirección, longitud y ángulo de apertura.    
        /// </summary>
        /// <returns></returns>
        public cElements GetSeenElements(double longitude, double latitude, double bearing, double maxDistance, double viewAngle, ref cElement sectorElement)
        {
            try
            {   
                tPoint3D p3dCurrent = new tPoint3D(longitude, latitude);
                tPoint3D p3dCurrentUTM = SpatialConversions.ConvertToUTM(p3dCurrent, miHuso);

                cElement oSector = new cElement();
                oSector.Type = eGeometryTypes.Polygon;
                oSector.Geometry.Add(p3dCurrentUTM);


                //Instead of using a line I am using use a sector in order to widen the view range
                double bearingTrig = SpatialConversions.GeograhicToTrigonometric(bearing, 1);

                int iNumPoints = 10;
                double dViewAngleRad = (viewAngle * Math.PI) / 360;
                double dAngle1 = bearingTrig + dViewAngleRad;
                double dStepAngle = dViewAngleRad / iNumPoints;

                for (int i = 0; i < iNumPoints; i++)
                {
                    double dAngle =  dAngle1 - dStepAngle * i;

                    tPoint3D oVertex = new tPoint3D(p3dCurrentUTM.x + maxDistance * Math.Cos(dAngle), p3dCurrentUTM.y + maxDistance * Math.Sin(dAngle));
                    oSector.Geometry.Add(oVertex);
                }
                oSector.Geometry.CloseRings();
                oSector.GetExtent(true);
                

                cElements oElements = moUTMGeoConcepts.CheckSpatial(oSector, eSearchingModes.Overlaps);

                cElements oSeenElements = new cElements();
                foreach (cElement oElement in oElements)
                {
                    cParts oParts =  oElement.MakeSpatialOperation(eSpatialOperationTypes.Intersection, oSector);
                    if(oParts != null)
                    {
                        oElement.Name = oElement.GetProperty("description").Value.ToString();
                        oSeenElements.Add(oElement);
                    }
                }
                sectorElement = oSector;
                sectorElement.Name = "View Area";
                return oSeenElements;

            }
            catch(Exception exc)
            {
                throw (exc);
            }
        }

        /// <summary>
        /// Devuelve los elementos que cortan completamente un segmento dado en forma de origen, dirección, longitud. 
        /// Es decir, los dos extremos del segmento deben estar fuera del elemento
        /// </summary>
        /// <param name="longitude">Longitud en coordenadas geográficas. Grados con fracciones.</param>
        /// <param name="latitude">Latitud en coordenadas geográficas. Grados con fracciones.</param>
        /// <param name="bearing">Dirección en formato geográfico, es decir Norte = 0º y sentido horario, y en radianes.</param>
        /// <param name="maxDistance">Distancia máxima en metros.</param>
        /// <returns></returns>
        public cElements GetIntersectedElements(double longitude, double latitude, double bearing, double maxDistance, cElements seenElements)
        {
            try
            {
                

                tPoint3D p3dCurrent = new tPoint3D(longitude, latitude);
                tPoint3D p3dCurrentUTM = SpatialConversions.ConvertToUTM(p3dCurrent, miHuso);
                cElement oLineElement = new cElement();
                oLineElement.Type = eGeometryTypes.Line;

                tPoint3D opt3d1 = new tPoint3D(p3dCurrentUTM.x, p3dCurrentUTM.y);
                tPoint3D opt3d2 = new tPoint3D(p3dCurrentUTM.x + maxDistance * Math.Sin(bearing), p3dCurrentUTM.y + maxDistance * Math.Cos(bearing));
                cElement oPt1 = new cElement();
                oPt1.Type = eGeometryTypes.Point;
                oPt1.Geometry.Add(opt3d1);
                cElement oPt2 = new cElement();
                oPt2.Type = eGeometryTypes.Point;
                oPt2.Geometry.Add(opt3d2);

                oLineElement.Geometry.Add(opt3d1);
                oLineElement.Geometry.Add(opt3d2);
                oLineElement.GetExtent(true);

                cElements oIntersectedElements = new cElements();

                //Sólo me interesan si tienen dos cortes, es decir si los dos extremos están fuera del recinto
                foreach (cElement oCandidateElement in seenElements)
                {


                    if(oCandidateElement.CheckSpatial(eSearchingModes.Crosses, oLineElement) && 
                        !oCandidateElement.IsPointIn2(opt3d1) && !oCandidateElement.IsPointIn2(opt3d2))
                    {
                        oIntersectedElements.Add(oCandidateElement);
                    }
                }

                return oIntersectedElements;
            }
            catch(Exception exc)
            {
                throw (exc);
            }
        }

        public double GetDistance(tPoint3D oPoint1, tPoint3D oPoint2)
        {
            try
            {
                return Math.Sqrt(Math.Pow(oPoint2.x - oPoint1.x, 2) + Math.Pow(oPoint2.y - oPoint1.y, 2));
            }
            catch (Exception exc)
            {
                throw (exc);
            }
        }

        /// <summary>
        /// Get the cElement most restricted to the given point in a collection of cElements
        /// </summary>
        /// <param name="cPoint">Point</param>
        /// <param name="cGeoElements">Collection of cElements</param>
        /// <returns></returns>
        public cElement getMostRelatedElement(tPoint3D tpPoint, cElements cGeoElements)
        {

            try
            {
                // Get the minimum area polygon from the collection
                double dMinArea = -1;
                double dActualArea = 0;
                cElement cMinAreaElement = null;
                foreach (cElement ceActualElement in cGeoElements)
                {
                    dActualArea = ceActualElement.GetArea();
                    if (tpPoint.CheckSpatial(eSearchingModes.Within,ceActualElement) && ((dMinArea == -1) || (dActualArea < dMinArea)))
                    {
                        cMinAreaElement = ceActualElement;
                        dMinArea = dActualArea;
                    }
                }
                double dRelativeOverlapping = 100;
                if (cMinAreaElement != null)
                {
                    cMinAreaElement.AddUserData(dRelativeOverlapping);
                }
                return cMinAreaElement;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        /// <summary>
        /// Get the cElement most related to a given cElement in a collection of cElements
        /// </summary>
        /// <param name="cGeoElement">cElement</param>
        /// <param name="cGeoElements">Collection of cElements</param>
        /// <returns></returns>
        public cElement getMostRelatedElement(cElement cGeoElement, cElements cGeoElements)
        {

            try
            {
                double dRelativeOverlap = 0;
                double dActualRelativeOverlap = 0;
                cElement ceMostRelatedElement = null;
                double dGeoElementArea = cGeoElement.GetArea();
                // Get the relative intersections of cGeoElement in all cGeoElements and
                // returns the largest relative overlap region to cGeoElement area.
                foreach (cElement ceActualElement in cGeoElements)
                {
                    if (cGeoElement.CheckSpatial(eSearchingModes.Overlaps, ceActualElement))
                    {
                        cParts cpIntersection = cGeoElement.MakeSpatialOperation(eSpatialOperationTypes.Intersection, ceActualElement);
                        if (cpIntersection != null)
                        {
                            dActualRelativeOverlap = cpIntersection.GetArea()/dGeoElementArea;
                            if (dActualRelativeOverlap > dRelativeOverlap)
                            {
                                dRelativeOverlap = dActualRelativeOverlap;
                                ceMostRelatedElement = ceActualElement;
                            }
                        }
                    }
                }
                if (ceMostRelatedElement != null)
                {
                    ceMostRelatedElement.AddUserData(dRelativeOverlap);
                }
                return ceMostRelatedElement;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        /// <summary>
        /// Calcula el solapamiento relativo entre dos elementos
        /// </summary>
        /// <param name="checkedElement">Elemento cuyo solapamiento se quiere calcular</param>
        /// <param name="mainElement">Elemento respecto del que se quiere calcular el solapamiento</param>
        /// <returns>Solapamiento relativo [0-1]</returns>
        public double CheckOverlapping(cElement checkedElement, cElement mainElement)
        {
            if (checkedElement.Equals(mainElement))            
                return 1;            
            else
            {
                if (!mainElement.CheckSpatial(eSearchingModes.Overlaps, checkedElement))
                    return 0;
                else
                {
                    double dMainElementArea = mainElement.GetArea();
                    cParts cpIntersection = mainElement.MakeSpatialOperation(eSpatialOperationTypes.Intersection, checkedElement);
                    if (cpIntersection == null)
                        return 0;
                    else                    
                        return cpIntersection.GetArea() / dMainElementArea;
                        
                }

            }
        }

    }
    #endregion SpatialOperations
}
